﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace StateObserver.Core.Fluent
{
    /// <summary>
    /// A very limited class, meant to be manipulated through the <see cref="StateObserver.Core.Fluent.FluentStateExtensions"/>.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class FluentStateObserver<T>
    {
        /// <summary>
        /// Gets the underlying observable.
        /// </summary>
        /// <value>The observable.</value>
        public IObservable<T> Observable { get; private set; }

        /// <summary>
        /// The list of matches found so far.
        /// </summary>
        public IEnumerable<T> CurrentChain { get { return Matches; } }

        /// <summary>
        /// Gets the index of the current state.
        /// </summary>
        /// <value>The index of the current state.</value>
        public int CurrentStateIndex { get { return Matches.Count; } }

        /// <summary>
        /// Gets or sets the matches.
        /// </summary>
        /// <value>The matches.</value>
        private IList<T> Matches { get; set; }

        /// <summary>
        /// Gets or sets the transitions.
        /// </summary>
        /// <value>The transitions.</value>
        internal IList<IList<Predicate<T>>> Transitions { get; set; }

        /// <summary>
        /// Gets the current transition.
        /// </summary>
        /// <value>The current transition.</value>
        internal IList<Predicate<T>> CurrentTransition { get { return Transitions.Count < 1 ? new List<Predicate<T>>() : Transitions[CurrentStateIndex]; } }

        /// <summary>
        /// Gets or sets the reset conditions. Keyed on the index of the corresponding transitions.
        /// </summary>
        /// <value>The reset conditions.</value>
        internal IDictionary<int, IEnumerable<Predicate<T>>> ResetConditions { get; set; }

        /// <summary>
        /// Gets or sets the error action. What to do on error.
        /// TODO: Add Extensions for setting this.
        /// </summary>
        /// <value>The error action.</value>
        internal Action<Exception> ErrorAction { get; set; }


        /// <summary>
        /// Gets or sets the completed action.  What to do on completion.
        /// TODO: Add Extensions for setting this.
        /// </summary>
        /// <value>The completed action.</value>
        internal Action CompletedAction { get; set; }

        /// <summary>
        /// Gets or sets the on match action.  What to do when a match occurs.
        /// </summary>
        /// <value>The on match action.</value>
        internal Action<FluentStateObserver<T>> OnMatchAction{ get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="FluentStateObserver"/> class.
        /// </summary>
        /// <param name="observable">The observable.</param>
        public FluentStateObserver(IObservable<T> observable)
        {
            Observable = observable;
            Matches = new List<T>();
            Transitions = new List<IList<Predicate<T>>>();
            ResetConditions = new Dictionary<int, IEnumerable<Predicate<T>>>();

            SetupProcessing();
        }

        /// <summary>
        /// Subscribe to the underlying observable anonymously.
        /// </summary>
        private void SetupProcessing()
        {
            Observable
                .Subscribe(OnGotElement, OnError, OnCompleted);
        }

        /// <summary>
        /// Called when the underlying observable publishes an item.
        /// </summary>
        /// <param name="element">The element published.</param>
        private void OnGotElement(T element)
        {
            if (CheckResetConditions(element))
            {
                DoReset();
                return;
            }

            if (CheckCurrentTransition(element))
            {
                DoTransition(element);
            }
        }

        /// <summary>
        /// Checks the current transition.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns>true is the current transition is satisfied, otherwise false.</returns>
        private bool CheckCurrentTransition(T element)
        {
            bool doTransition = true;
            foreach (var cond in CurrentTransition)
            {
                if (!cond.Invoke(element))
                {
                    doTransition = false;
                    break;
                }
            }

            return doTransition;
        }

        /// <summary>
        /// Called when [error].
        /// </summary>
        /// <param name="ex">The ex.</param>
        private void OnError(Exception ex)
        {
            if ( ErrorAction != null )
                ErrorAction.Invoke(ex);
        }

        /// <summary>
        /// Called when [completed].
        /// </summary>
        private void OnCompleted()
        {
            if (CompletedAction != null)
                CompletedAction.Invoke();
        }

        /// <summary>
        /// Checks the reset conditions.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns>True is the reset condition was satisfied.</returns>
        private bool CheckResetConditions(T element)
        {
            if (!ResetConditions.ContainsKey(CurrentStateIndex))
                return false;

            bool foundReset = false;
            foreach (var pred in ResetConditions[CurrentStateIndex])
            {
                if (pred.Invoke(element))
                {
                    foundReset = true;
                    break;
                }
            }

            return foundReset;
        }

        /// <summary>
        /// Resets the chain.
        /// </summary>
        private void DoReset()
        {
            Matches.Clear();
        }

        /// <summary>
        /// Does the transition.  Records the matched element.
        /// </summary>
        /// <param name="element">The element.</param>
        private void DoTransition(T element)
        {
            Matches.Add(element);

            if (CurrentStateIndex >= Transitions.Count)
            {
                DoMatchAction();
            }
        }

        /// <summary>
        /// Does the match action.
        /// </summary>
        private void DoMatchAction()
        {
            if (OnMatchAction != null)
                OnMatchAction.Invoke(this);

            DoReset();
        }

        /// <summary>
        /// Adds the transitions.
        /// </summary>
        /// <param name="transitions">The transitions.</param>
        internal void AddTransitions(params Predicate<T>[] transitions)
        {
            this.AddTransitions(transitions.ToList());
        }

        /// <summary>
        /// Adds the transitions.
        /// </summary>
        /// <param name="transitions">The transitions.</param>
        /// <param name="resets">The resets.</param>
        internal void AddTransitions(IEnumerable<Predicate<T>> transitions, IEnumerable<Predicate<T>> resets)
        {
            ResetConditions.Add(Transitions.Count, resets);
            this.AddTransitions(transitions.ToList());            
        }

        /// <summary>
        /// Adds the transitions.
        /// </summary>
        /// <param name="transitions">The transitions.</param>
        internal void AddTransitions(IList<Predicate<T>> transitions)
        {
            this.Transitions.Add(transitions);
        }

    }

    /// <summary>
    /// Extensions for matching patterns in Observables.
    /// </summary>
    public static class FluentStateExtensions
    {
        /// <summary>
        /// Sets the first transition case.
        /// </summary>        
        /// <param name="transition">The transitions.</param>        
        public static FluentStateObserver<T> StartsWith<T>(this FluentStateObserver<T> obs, params Predicate<T>[] transition)
        {
            obs.Transitions.Clear();
            obs.AddTransitions(transition);

            return obs;
        }

        /// <summary>
        /// Sets the next transition case.
        /// </summary>        
        /// <param name="transition">The transitions.</param>  
        public static FluentStateObserver<T> FollowedBy<T>(this FluentStateObserver<T> obs, params Predicate<T>[] transition)
        {
            obs.AddTransitions(transition);

            return obs;
        }

        /// <summary>
        /// Sets a reset condition on the previously specified transition.
        /// </summary>        
        /// <param name="transition">The reset conditions.</param>  
        public static FluentStateObserver<T> ResetOn<T>(this FluentStateObserver<T> obs, params Predicate<T>[] resets)
        {
            var prevTransition = obs.Transitions.LastOrDefault();
            if (prevTransition != null)
            {
                obs.Transitions.Remove(prevTransition);
                obs.AddTransitions(prevTransition, resets);
            }

            return obs;
        }

        /// <summary>
        /// Sets the transition case to be followed immediately following the previously specified transition.
        /// </summary>        
        /// <param name="transition">The transitions.</param>  
        public static FluentStateObserver<T> FollowedImmediatelyBy<T>(this FluentStateObserver<T> obs, params Predicate<T>[] transition)
        {
            obs.AddTransitions(transition, new List<Predicate<T>>() { NotMatch(transition) });

            return obs;
        }

        /// <summary>
        /// Sets a final transition case.
        /// </summary>        
        /// <param name="transition">The transitions.</param>  
        public static FluentStateObserver<T> EndsWith<T>(this FluentStateObserver<T> obs, params Predicate<T>[] transition)
        {
            obs.AddTransitions(transition);

            return obs;
        }

        /// <summary>
        /// Sets the final transition case to be matched immediately following the previously specified transition.
        /// </summary>        
        /// <param name="transition">The transitions.</param>  
        public static FluentStateObserver<T> EndsImmediatelyWith<T>(this FluentStateObserver<T> obs, params Predicate<T>[] transition)
        {
            obs.AddTransitions(transition, new List<Predicate<T>>() { NotMatch(transition) });

            return obs;
        }

        /// <summary>
        /// What to do when a match occurs.
        /// </summary>
        /// <param name="act">The Action to perform.</param>        
        public static FluentStateObserver<T> OnMatch<T>(this FluentStateObserver<T> obs, Action<FluentStateObserver<T>> act)
        {
            obs.OnMatchAction = act;

            return obs;
        }

        /// <summary>
        /// Builds a Predicate that returns true if any of the passed in matches fail.
        /// </summary>        
        /// <param name="matches">The matches that must be true.</param>
        /// <returns>A Predicate that returns true if any of the passed in matches fail.</returns>
        internal static Predicate<T> NotMatch<T>(IEnumerable<Predicate<T>> matches)
        {            
            return x => 
            {
                foreach ( var match in matches )
                {
                    if ( !match(x) )
                    {
                        return true;
                    }
                }
                return false;            
            };
        }
    }
}
